/**
 * 
 */
package com.googlecode.cswish.util;

import java.beans.Introspector;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.TypeVariable;
import java.util.Arrays;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.WeakHashMap;

import org.apache.commons.io.IOUtils;
import org.apache.log4j.Logger;
import org.springframework.cglib.proxy.Enhancer;

import com.googlecode.cswish.annotation.GenericType;
import com.googlecode.cswish.annotation.ParamScope;
import com.googlecode.cswish.asm.ClassReader;
import com.googlecode.cswish.asm.Label;
import com.googlecode.cswish.asm.MethodVisitor;
import com.googlecode.cswish.asm.Type;
import com.googlecode.cswish.asm.commons.EmptyVisitor;
import com.sun.beans.TypeResolver;

/**
 * Use method.getParameterNames() after JDK 1.7
 * <p>
 * Analyze the class byte code and get the extra method information: parameter name & generic type
 * 
 * @author Jerry.Feng Date: 2008-4-4
 */
public class ClassEx {
	private static final Logger logger = Logger.getLogger(ClassEx.class);
	
	private static final WeakHashMap<Adapter, ExtraMethodInfo> cache
		= new WeakHashMap<Adapter, ExtraMethodInfo>();
	
	/**
	 * Get the parameter name and add the protected prefix to the protected parameter name
	 * 
	 * @param method
	 * @return
	 */
	public static String[] getProtectedParameterNames(Method method) {
		String[] parameterNames = getParameterNames(method);
		
		Annotation[][] allAnnotations = method.getParameterAnnotations();
		for (int i = 0; i < allAnnotations.length; i++) {
			Annotation[] annotations = allAnnotations[i];
			for (Annotation annotation : annotations) {
				if (annotation instanceof ParamScope) {
					ParamScope paramScope = (ParamScope) annotation;
					if (!paramScope.visible()) {
						parameterNames[i] = FrameConstant.PARAM_PROCTED_NAME + parameterNames[i];
						break;
					}
				}
			}
		}
		return parameterNames;
	}
	
	public static String[] getParameterNames(Method method) {
		Class clazz = method.getDeclaringClass();
		boolean isCorrectName = false;
		String[] parameterNames = null;
		while (!isCorrectName && method != null && clazz != null){
			parameterNames = getParameterNames(new MethodAdapter(method));
			isCorrectName = true;
			for (String parameterName : parameterNames) {
				if (parameterName == null) {
					isCorrectName = false;
					break;
				}
			}
			
			if (!isCorrectName) {
				clazz = clazz.getSuperclass();
				if (clazz != null && !clazz.equals(Object.class)) {
					try {
						method = clazz.getDeclaredMethod(method.getName(), method.getParameterTypes());
					} catch (Exception ex) {
						method = null;
					}
				}
			}
		}
		return parameterNames;
	}
	
	public static String[] getParameterNames(Constructor method) {
		return getParameterNames(new ConstructorAdapter(method));
	}
	
	/**
	 * Get parameter name
	 * 
	 * @param adapter
	 * @return
	 */
	public static String[] getParameterNames(Adapter adapter) {
		String[] parameterNames = null;
		if (adapter.getParameterTypes().length > 0) {
			Class clazz = adapter.getDeclaringClass();

			if (Enhancer.isEnhanced(clazz)) {
				adapter.useSuperAdapter();
			}
			ExtraMethodInfo extraInfo = analyseClassInfo(adapter);
			if (extraInfo != null) {
				parameterNames = extraInfo.paramNames;
			} else {
				logger.warn("Parameter information is missed, please use 'javac -g' to compile the class");
				Class<?>[] parameterTypes = adapter.getParameterTypes();
				parameterNames = new String[parameterTypes.length];
				int i = 0;
				for (Class<?> parameterType : parameterTypes) {
					parameterNames[i++] = Introspector
							.decapitalize(parameterType.getSimpleName());
				}
			}
		} else {
			parameterNames = new String[0];
		}
		return parameterNames;
	}

	protected static ExtraMethodInfo analyseClassInfo(Adapter adapter) {
		ExtraMethodInfo extraInfo = cache.get(adapter);
		if (extraInfo != null) {
			return extraInfo;
		}
		// TODO: throw exception if adapter.getDeclaringClass().isInterface()==true
		MethodAnalysis analysis = new MethodAnalysis(adapter.getName(), 
				adapter.getDescriptor(), adapter.getParameterTypes().length);

		String name = null;
		InputStream input = null;
		try {
			name = adapter.getDeclaringClass().getName();
			input = ClassUtil.getResourceAsStream(name.replace('.', '/') + ".class", ClassEx.class);
			ClassReader cr = new ClassReader(input);
			cr.accept(analysis, 0);
		} catch (IOException ex) {
			logger.error("Fail to load class:" + name, ex);
		} catch (ClassExBreakPoint ex) {
			if (logger.isDebugEnabled()) {
				logger.debug("stop to parse class:" + name);
			}
		} finally {
			IOUtils.closeQuietly(input);
		}
		extraInfo = analysis.extraInfo;
		
		// Update the generic information according to the standard jdk5.0 API
		updateGenericInfo(adapter.getDeclaringClass(), adapter.getMethod(), adapter.getConstructor(), extraInfo);
		
		cache.put(adapter, extraInfo);
		return extraInfo;
	}
	
	private static void updateGenericInfo(Class<?> clazz, Method method, Constructor constructor, ExtraMethodInfo extraInfo) {
		java.lang.reflect.Type[] types;
		java.lang.reflect.Type retType;
		if (method != null) {
			types = method.getGenericParameterTypes();
			retType = method.getGenericReturnType();
		} else {
			types = constructor.getGenericParameterTypes();
			retType = null;
		}
		
		Map<String, String> actualTypeArguments = getActualTypeArguments(clazz);
		for (int i = 0; i < types.length; i++ ) {
			java.lang.reflect.Type type = types[i];
			if (type instanceof ParameterizedType) {
				String genericType = getGenericType(type, actualTypeArguments);
				if (genericType != null) {
					extraInfo.paramGenericTypes[i] = genericType;
				}
			}
		}
		
		if (retType != null && retType instanceof ParameterizedType) {
			String genericType = getGenericType(retType, actualTypeArguments);
			if (genericType != null) {
				extraInfo.retGenericType = genericType;
			}
		}
	}
	
	public static Map<String, String> getActualTypeArguments(Class<?> clazz) {
		Map<String, String> actualTypeArguments = new HashMap<String, String>();
		addActualTypeArguments(clazz, actualTypeArguments);
		return actualTypeArguments;
	}
	
	private static void addActualTypeArguments(Class<?> clazz, Map<String, String> actualTypeArguments) {
		java.lang.reflect.Type type = clazz.getGenericSuperclass();
		if (type instanceof ParameterizedType) {
			ParameterizedType parameterizedType = (ParameterizedType) type;
			java.lang.reflect.Type[] typeArguments = parameterizedType.getActualTypeArguments();
			
			Class<?> superClazz = clazz.getSuperclass();
			TypeVariable<?>[] vars = superClazz.getTypeParameters();
			
			for (int i = 0; i < typeArguments.length; i++) {
				String name = vars[i].getName();
				if (!actualTypeArguments.containsKey(name)) {
					actualTypeArguments.put(name, getGenericType(typeArguments[i], actualTypeArguments));
				}
			}
			
			addActualTypeArguments(superClazz, actualTypeArguments);
		}
	}
	
	public static String getGenericType(java.lang.reflect.Type type, Map<String, String> actualTypeArguments) {	
		return getGenericType(type, actualTypeArguments, new IdentityHashMap<Object, Object>());
	}
	
	private static String getGenericType(java.lang.reflect.Type type, Map<String, String> actualTypeArguments, Map<Object, Object> deadCycleCheck) {
		if (deadCycleCheck.containsKey(type)) {
			return null;
		}
		
		String genericType = null;
		
		if (type instanceof Class) {
			genericType = ((Class<?>)type).getName();
		} else if (type instanceof ParameterizedType) {
			ParameterizedType parameterizedType = (ParameterizedType) type;
			java.lang.reflect.Type[] typeArgs = parameterizedType.getActualTypeArguments();
			if (typeArgs != null && typeArgs.length > 0) {
				deadCycleCheck.put(type, type);
				genericType = getGenericType(typeArgs[0], actualTypeArguments, deadCycleCheck);
			} 
			if (genericType == null) {
				deadCycleCheck.put(type, type);
				genericType = getGenericType(parameterizedType.getRawType(), actualTypeArguments, deadCycleCheck);
			}
		} else if (type instanceof TypeVariable<?>) {
			TypeVariable<?> typeVariable = (TypeVariable<?>)type;
			String name = typeVariable.getName();
			genericType = actualTypeArguments.get(name);
			
			if (genericType == null) {
				java.lang.reflect.Type[] bounds = typeVariable.getBounds();
				if (bounds != null && bounds.length > 0) {
					deadCycleCheck.put(type, type);
					genericType = getGenericType(bounds[0], actualTypeArguments, deadCycleCheck);
				}
				if (genericType == null) {
					Object object = typeVariable.getGenericDeclaration();
					if (object instanceof Class) {
						genericType = ((Class<?>)object).getName();
					}
				}
			}
		}
		return genericType;
	}
	
	public static String[] getAnnotationGenericTypes(Method method) {
		Annotation[][] allAnnotations = method.getParameterAnnotations();
		String[] genericTypes = null;
		for (int i = 0; i < allAnnotations.length; i++) {
			Annotation[] annotations = allAnnotations[i];
			for (int j = 0; j < annotations.length; j++) {
				Annotation annotation = annotations[j];
				if (annotation instanceof GenericType) {
					if (genericTypes == null) {
						genericTypes = new String[allAnnotations.length];
					}
					
					String genericType = ((GenericType) annotation).value();
					if (GenericType.PARAM_MODEL.equals(genericType)) {
						genericType = GenericType.PARAM_MODEL;
					} else if (GenericType.MODEL.equals(genericType)) {
						genericType = GenericType.MODEL;
					}
					genericTypes[i] = genericType;
					break;
				}
			}
		}
		return genericTypes;
	}

	/**
	 * The last element is the return type if method has return type
	 * 
	 * @param method
	 * @return
	 */
	public static String[] getGenericTypes(Method method) {
		MethodAdapter methodAdapter = new MethodAdapter(method);
		if (Enhancer.isEnhanced(method.getDeclaringClass())) {
			methodAdapter.useSuperAdapter();
		}
		ExtraMethodInfo extraInfo = analyseClassInfo(methodAdapter);
		if (extraInfo != null) {
			 String[] types = copyOf(extraInfo.paramGenericTypes,
					 extraInfo.paramGenericTypes.length + 1);
			 types[types.length - 1] = extraInfo.retGenericType;
			return types;
		}
		return null;
	}
	
	// improve the performance of the original getGenericType 
	// FIXME: the method doesn't finish
	@Deprecated
	public static String getGenericType(Class base, java.lang.reflect.Type type) {
		String signature = type.toString();
		int index = signature.indexOf('<');
		Class genericClass = null;
		String genericType;
		if (index >= 0) {
			int end = signature.indexOf('<', index + 1); 		// such as List<Element<?>>
			if (end == -1) {
				end = signature.length() - 1;
			}
			genericType = signature.substring(index + 1, end);
			
			int packageIndex = genericType.indexOf('.');
			if (packageIndex > 0) {		// fast check				
				genericClass = ClassUtil.getClass(genericType);
			}
			
			if (genericClass == null) {
				genericType = null;
				
				TypeVariable[] vars = base.getTypeParameters();
				if (vars != null) {
					for (int i = 0; i < vars.length; i++) {
						if (genericType.equals(vars[i].getName())) {
							// FIXME: refer the original method
							break;
						}
					}
				}
				if (genericType == null) {
					logger.error("Can't find the generic type for " + base.getName() + '#' + type);
				}
			}
		} else {
			genericType = signature;
		}
		return genericType;
	}
	
	public static Class getPropertyType(Class base, java.lang.reflect.Type type) {
		Class clazz = TypeResolver.erase(TypeResolver.resolveInClass(base, type));
		return clazz;
    }
	
	/**
	 * @see Arrays.copyOf (JDK 1.6)
	 */
	public static <T,U> T[] copyOf(U[] original, int newLength) {
		Class<? extends T[]> newType = (Class<? extends T[]>) original.getClass(); 
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }
	
	interface Adapter {
		Method getMethod();
		Constructor getConstructor();
		String getName();
		Class<?>[] getParameterTypes();
		String getDescriptor();
		Class<?> getDeclaringClass();
		void useSuperAdapter();
	}
	
	static public class MethodAdapter implements Adapter {
		private Method method;
		
		public MethodAdapter(Method method) {
			this.method = method;
		}
		@Override
		public Class<?> getDeclaringClass() {
			return method.getDeclaringClass();
		}
		@Override
		public String getDescriptor() {
			return Type.getMethodDescriptor(method);
		}
		@Override
		public String getName() {
			return method.getName();
		}
		@Override
		public Class<?>[] getParameterTypes() {
			return method.getParameterTypes();
		}
		@Override
		public void useSuperAdapter() {
			Class<?> clazz = getDeclaringClass().getSuperclass();
			try {
				method = clazz.getMethod(method.getName(), method
						.getParameterTypes());
			} catch (Exception ex) {
				logger.debug("can't find method:" + method.getName());
			}
		}
		@Override
		public Constructor getConstructor() {
			return null;
		}
		@Override
		public Method getMethod() {
			return method;
		}
	}
	
	static public class ConstructorAdapter implements Adapter {
		private Constructor constructor;
		
		public ConstructorAdapter(Constructor constructor) {
			this.constructor = constructor;
		}
		@Override
		public Class<?> getDeclaringClass() {
			return constructor.getDeclaringClass();
		}
		@Override
		public String getDescriptor() {
			return Type.getConstructorDescriptor(constructor);
		}
		@Override
		public String getName() {
			return "<init>";
		}
		@Override
		public Class<?>[] getParameterTypes() {
			return constructor.getParameterTypes();
		}
		@Override
		public void useSuperAdapter() {
			Class<?> clazz = getDeclaringClass().getSuperclass();
			try {
				constructor = clazz.getConstructor(constructor.getParameterTypes());
			} catch (Exception ex) {
				logger.debug("can't find method:" + constructor.getName());
			}
		}
		@Override
		public Constructor getConstructor() {
			return constructor;
		}
		@Override
		public Method getMethod() {
			return null;
		}
	}
	
	static public class MethodAnalysis extends EmptyVisitor {
		
		private String methodName;
		private String methodDesc;
		
		private ExtraMethodInfo extraInfo;
		private boolean found;
		
		public MethodAnalysis(String methodName, String methodDesc, int paramLen) {
			this.methodName = methodName;
			this.methodDesc = methodDesc;
			extraInfo = new ExtraMethodInfo();
			extraInfo.paramNames = new String[paramLen];
			extraInfo.paramGenericTypes = new String[paramLen];
		}

		public MethodVisitor visitMethod(int access, String name, String desc,
				String signature, String[] exceptions) {
			if (found) {
				// use exception to force the intercept to stop the analysis
				throw new ClassExBreakPoint();
			}
			
			if (name.equals(this.methodName) && desc.equals(this.methodDesc)) {
				this.extraInfo.retGenericType = getRetGenericType(signature);
				found = true;
				return this;
			}
			return null;
		}
		
		public String getRetGenericType(final String signature) {
			if (signature != null) {
				int index = signature.lastIndexOf(')');
				String retSignature = signature.substring(index + 1);
				// Only return the last type as the generic type
				return getGenericType(retSignature);
			}
	        return null;
	    }
		
		private String getGenericType(String signature) {
			if (signature == null) {
				return null;
			}
			
			int begin = signature.lastIndexOf('<');
			if (begin > 0) {
				int end = signature.indexOf('>', begin);
				
				return signature.substring(begin + 1, end).replace('/', '.');
			}
			return null;
		}
		
		public void visitLocalVariable(
		        final String name,
		        final String desc,
		        final String signature,
		        final Label start,
		        final Label end,
		        final int index) {
			if (index <= extraInfo.paramNames.length && index > 0) {
				extraInfo.paramNames[index - 1] = name;
				extraInfo.paramGenericTypes[index - 1] = getGenericType(signature);
			}
	    }
	}
	
	// a structure to store the extra method information
	static class ExtraMethodInfo {
		public String[] paramNames;
		public String[] paramGenericTypes;
		public String retGenericType;
	}
}
